/*
 * Javascript to control the CastDR Chrome application 
 *
 * This controls the loading of the information from dr.dk 
 */
(function() {
    'use strict';

    //================================================================
    /** 
     * Base of dr.dk/nu API 
     * @name API_URL 
     */
    var API_URL     = "https://www.dr.dk/nu-mobil/api/";
    /**
     * Maximum number of series per genre to get *
     * @name MAX_GENRE
     */
    var MAX_GENRE   = 100;
    /**
     * Maximum number of series per letter to get *
     * @name MAX_LETTER
     */
    var MAX_LETTER  = 100;
    /**
     * Maximum number of series per channel to get *
     * @name MAX_CHANNEL
     */
    var MAX_CHANNEL = 100;
    /**
     * Maximum number of series for newest to get *
     * @name MAX_NEWEST
     */
    var MAX_NEWEST  = 100; // 10
    /**
     * Platform to use to resolve the stream URLs *
     * @name PLATFORM
     */
    var PLATFORM    = "android";

    //================================================================
    // For local debugging
    WrapXHR.setDebug(false);
    WrapXHR.setRouter("../../lib/php/wrapxhr.php");

    //================================================================
    /** 
     * Beautify logs 
     *
     * @name Log
     * @namespace 
     */
    var Log = { 
	/** 
	 * Errors 
	 * @fieldOf Log
	 */
	e: console.error.bind(console, "%cNuLoader", "color:#c00"), 
	/** 
	 * Warnings 
	 * @fieldOf Log
	 */
	w: console.warn.bind(console, "%cNuLoader", "color:#cc0"), 
	/** Information 
	 * @fieldOf Log
	 */
	i: console.log.bind(console, "%cNuLoader", "color:#00c"),
	/** Debug  
	 * @fieldOf Log
	 */
	d: console.log.bind(console, "%cNuLoader", "color:#c0c")
	// i: function () {}
    };

    //================================================================
    /** 
     * Channels 
     * 
     * @name ChMap
     * @enum 
     */
    var ChMap = {
	/** DR1 */
	"dr1" : "DR1",
	/** DR2 */
	"dr2" : "DR2",
	/** DR3 */
	"dr3" : "DR3",
	/** DR-K */
	"dr-k" : "DR-K",
	/** DR-Ultra */
	"dr-ultra" : "DR-Ultra",
	/** DR-Ramasjang */
	"dr-ramasjang" : "DR-Ramasjang"
    };
    //================================================================
    /** 
     * Utilities.  Only has static member functions.  
     *
     * @name Util
     * @class 
     * @constructor 
     */
    var Util = function() { }
    //_________________________________________________________________
    /** 
     * Test if a given node has a particular class
     * 
     * @param {HTMLElement}  node Node
     * @param {String}  name      Class name 
     * 
     * @return true if the node has the relevant class
     */
    Util.hasClass = function(node, name) {
	return new RegExp("(?:^|\\s+)" + name + "(?:\\s+|$)")
	    .test(node.className);
    };
    //_________________________________________________________________
    /** 
     * Add a class to a node 
     * 
     * @param {HTMLElement} node Node
     * @param {String} name      Class to add 
     */
    Util.addClass = function(node, name) {
	if (node == null) return;
	if (this.hasClass(node, name)) return;
	node.className = (node.className ? [ node.className, name ].join(' ')
                          : name);
    };
    //_________________________________________________________________
    /** 
     * Remove a given class from a node 
     * 
     * @param {HTMLElement} node Node 
     * @param {String} name      Class name
     */
    Util.removeClass = function(node, name) {
	if (node == null) return;
	if (!this.hasClass(node, name)) return;
	var c = node.className;
	node.className = c.replace(new RegExp("(?:^|\\s+)" + name 
					      + "(?:\\s+|$)", "g"), "");
    };
    //_________________________________________________________________
    /** 
     * Parse the response from an XMLHttpRequest (possibly proxied) 
     * 
     * @param {String} txt response text of the XMLHttpRequest 
     * 
     * @return JSON object or null on error 
     */
    Util.parseResponse = function(txt) { 
	// Sanitize for tags
	if (txt == null || txt.length <= 0) { 
	    Log.e("Got empty response!");
	    return null;
	}
	var res = txt;
	
	do {
	    txt = res;
	    res = txt.replace(/<\/?[a-zA-Z0-9]+>/, "");
	} while (txt != res);
	txt = res;
	res = txt;
	do {
	    txt = res;
	    res = txt.replace(/[\n\r\f]+/, "&#10;");
	} while (txt != res);
	txt = res;

	try {
	    var json = JSON.parse(txt);
	    // var req  = null;
	    // if (txt[0] == '[') req = json[json.length-1]["_req"];
	    // else               req = json["_req"];
	    // Log.d("Got response to request: " + req);
	    return json;
	}
	catch (e) {
	    Log.e('Invalid JSON data: ' + e.toString() + ': "' + txt + '" ');
	}
	return null;
    }
    // ================================================================
    /**
     * A class representing a series
     *
     * @public 
     * @name Serie 
     * @class 
     * @constructor 
     *
     * @param json Retrieved JSON object.  We decode this to set our
     * member variables.
     *
     * @return Newly allocated object of this class
     */
    var Serie = function(json) {
	/**
         * The slug
         * @fieldOf Serie.prototype
         */
	this.mSlug = json["ProgramSeriesSlug"];
	/**
         * Title
         * @fieldOf Serie.prototype
         */
	this.mTitle = json["Title"];
	/**
         * Genre
         * @fieldOf Serie.prototype
         */
	this.mGenre = json["OnlineGenreText"];
	/**
         * information URL
         * @fieldOf Serie.prototype
         */
	this.mUrl = json["Url"];
	/**
         * Channel
         * @fieldOf Serie.prototype
         */
	this.mChannel = ChMap[json["ChannelSlug"]];
	/**
         * Image
         * @fieldOf Serie.prototype
         */
	this.mImg = json["Image"];
	/**
         * First letter
         * @fieldOf Serie.prototype
         */
	this.mLetter = '-';
	/**
         * List of programs
         * @fieldOf Serie.prototype
         */
	this.mPrograms = [];
	/** 
	 * Image node 
	 * @fieldOf Serie.prototype
	 */
	this.mImgNode = null;
	/**
         * List node
         * @fieldOf Serie.prototype
         */
	this.mListNode = null;
	/**
         * Progress node
         * @fieldOf Serie.prototype
         */
	this.mProgressNode = null;
	/**
         * Number of image failures
         * @fieldOf Serie.prototype
         */
	this.mImgNerr = 0;

	if (this.mSlug == null || this.mSlug.length <= 0)
            this.mSlug = json["programSerieSlug"];
	this.initialize(json);
    };
    //_________________________________________________________________
    /**
     * Intialize this object
     *
     * @param json Retrieved information 
     */
    Serie.prototype.initialize = function(json) {
	if (this.mSlug    == null) this.mSlug    = json["programSerieSlug"];
	if (this.mTitle   == null) this.mTitle   = json["title"];
	if (this.mGenre   == null) this.mGenre   = "Ingen";
	if (this.mChannel == null) this.mChannel = "DR1";
	if (this.mTitle   != null) this.mLetter  = this.mTitle.charAt(0);
	this.mLetter = this.mLetter.toUpperCase();
	var first = this.mLetter;
	if (first.toLowerCase() == first)
	    // Not a character
	    this.mLetter = '-';
	else { 
	    if      (this.mLetter == 'Æ' || 
		     this.mLetter == '%U00C6') this.mLetter = '&AElig;';
	    else if (this.mLetter == 'Ø' || 
		     this.mLetter == '%U00D8') this.mLetter = '&Oslash;';
	    else if (this.mLetter == 'Å' || 
		     this.mLetter == '%U00C5') this.mLetter = '&Aring;';
	}
    };
    //_________________________________________________________________
    /** 
     * Check if we have the fields we need
     * 
     * @return true if all is good 
     */
    Serie.prototype.isValid = function() {
	return !(this.mSlug    == null || 
		 this.mTitle   == null || 
		 this.mGenre   == null || 
		 this.mChannel == null || 
		 this.mLetter  == null);
    };
    //_________________________________________________________________
    /**
     * Create an element with the needed sub-elements. 
     *
     * Sub-elements are created using Document#createElement so  
     * that we can save pointers to the elements (rather than 
     * finding them later on).  
     */
    Serie.prototype.createElement = function() { 
	var el = document.createElement("div");
	el.className = "serie";
	el.id        = this.mSlug;

	var cn = document.createElement("div");
	cn.className = "serieContent";
	el.appendChild(cn);

	var hd = document.createElement("div");
	hd.className = "serieHeader";
	hd.addEventListener("click", this.handle.bind(this));
	cn.appendChild(hd);

	var im = document.createElement("div");
	im.className = "serieImg";
	hd.appendChild(im);

	this.mImgNerr = 0;
	this.mImgNode = document.createElement("img");
	this.mImgNode.src = this.mImg;
	this.mImgNode.alt = this.mTitle;
	this.mImgNode.addEventListener("error", this.imgError(this));
	im.appendChild(this.mImgNode);

	var bd = document.createElement("div");
	bd.className = "serieBody";
	hd.appendChild(bd);
	
	var ti = document.createElement("div");
	ti.className = "serieTitle";
	ti.innerHTML = this.mTitle;
	bd.appendChild(ti);

	var ge = document.createElement("div");
	ge.className = "serieGenre";
	ge.innerHTML = "Genre: " + this.mGenre;
	bd.appendChild(ge);

	var ch = document.createElement("div");
	ch.className = "serieChannel";
	ch.innerHTML = "Vist p&aring; " + this.mChannel;
	bd.appendChild(ch);

	var li = document.createElement("div");
	li.className = "serieLink";
	li.innerHTML = "Mere";
	li.addEventListener("click", this.browseInfo.bind(this));
	bd.appendChild(li);
	
	this.mProgressNode = document.createElement("progress");
	this.mProgressNode.className = "serieProgress";
	cn.appendChild(this.mProgressNode);

	this.mListNode = document.createElement("div");
	this.mListNode.className = "seriePrograms";
	cn.appendChild(this.mListNode);

	return el;
    }
    //_________________________________________________________________
    /**
     * Case the image couldn't be loaded.  Note, we put in a counter
     * of the number of times we can fail before substituting the
     * default image.  It seems like when we add the DOM image element
     * this is called almost straight away.  However, we should really
     * wait until the element has rendered completely
     */
    Serie.prototype.imgError = function() { 
	Log.d("Image " + this.mImgNode.src + " not found " + this.mImgNerr);
	this.mImgNerr++;
	if (this.mImgNerr < 3) return;
	this.mImgNode.src = 'default.png';
	this.mImgNode.onerror = 'function() {}';
    }
    //_________________________________________________________________
    /** 
     * Handle click on the series.  This will toggle the display
     * programs in this serie.
     */
    Serie.prototype.handle = function () { 
	if (Util.hasClass(this.mListNode, "shown")) 
	    this.close();
	else 
	    this.open();
    }
    //_________________________________________________________________
    /** 
     * Close the display of programs 
     */
    Serie.prototype.close = function () { 
	Util.removeClass(this.mListNode, "shown");
    }    
    //_________________________________________________________________
    /** 
     * Browse a information about this serie.  We first try to load
     * the URL we got from the original request NuLoader.request.
     * If that fails, we fall back to a search on dr.dr.
     * 
     * @param url URL to browse 
     */
    Serie.prototype.browseInfo = function() {
	window.event.stopPropagation();

	NuLoader.request('http://dr.dk/' + this.mUrl, 
			 true, this.onBrowse.bind(this),
			 this.onBrowse.bind(this, null),true);
	return false;
    };
    //_________________________________________________________________
    /** 
     * Call-back for browsing serie information 
     * 
     * @param {String} data The data returned from the request
     */
    Serie.prototype.onBrowse = function(data) {
	var url = 'http://dr.dk/' + this.mUrl;
	if (data == null) {
	    Log.w("Failed to get data");
	    url  = url.replace('tv/program/', 'search/Result?query=');
	}
	var p = window.open(url,"_blank","menubar=no,status=no,titlebar=no",
			    true);
    };
    //_________________________________________________________________
    /** 
     * Show the links.  If the information hasn't been loaded yet, we
     * do that here.
     */
    Serie.prototype.open = function() {
	if (this.mPrograms.length) {
	    Util.addClass(this.mListNode, "shown");
            return;
	}
	// if (this.mLoading) { 
	//   alert('Vent indtil informationen er hentet'); 
	//   return;
	// }
	// node.style.cursor = "n-resize";
	// this.mLoading = true;
	this.mProgressNode.style.display = 'block';

	NuLoader.request(API_URL + "/programserie?slug=" + this.mSlug, true, 
			 this.onLinks.bind(this), 
			 this.onLinks.bind(this,null));

	return false;
    };
    //_________________________________________________________________
    /** 
     * Call-back for show links. 
     * 
     * @param {JSON} json The JSON object from the request
     */
    Serie.prototype.onLinks = function(json) {
	if (json == null) return;
	try {
	    var progs = json["videos"];
	}
	catch (e) { 
	    Log.e(e);
	    return;
	}
	this.mProgressNode.max   = progs.length;
	this.mProgressNode.value = 0;
	this.onLink(progs, 0, null, null);
    };
    //_________________________________________________________________
    /** 
     * Get video links.  This is called recursively until we have
     * dealt with all programs in this series.  At the final call we
     * call Serie#writePrograms to render the actuall programs
     * retrieved.  
     *
     * On the first call (and if retrieval fails) @a json is null, @a
     * idx points to the first entry in @a progs, and @a p is null.
     *
     * On subsequent calls, @a json is our data for
     * information on the program videos, @a idx points to the next
     * program in @a progs to retrieve information for, and @a p is
     * the program for which we made a request.
     *
     * If on some call @a idx points to or beyond the end of @a progs,
     * we are done and can render the actual list of programs.
     *
     * If we at some cannot make a valid Program object, we pass call 
     * this member function again but with an empty request and program 
     * object.
     * 
     * @param {JSON}    json Possible result of request 
     * @param {Array}   progs List of programs 
     * @param {Integer}     idx Current index 
     * @param {Program}       p Current program
     */
    Serie.prototype.onLink = function(progs,idx,p,json) {
	if (json) {
	    p.mDuration = json["duration"];
	    var streams = json["streams"];
	    if (streams == null) { 
		Log.w("Didn't get any streams");
	    }
	    else {
		p.setLink("low",    streams["low"]);
		p.setLink("medium", streams["medium"]);
		p.setLink("high",   streams["high"]);	
	    }
	}
	if (idx >= progs.length) { 
	    // No more elements 
	    Log.d("Done getting links, now write");
	    this.writePrograms();
	    return;
	}
	var p = new Program(progs[idx]);
	if (!p.isValid()) { 
	    Log.w("Got invalid program, skipping to next one");
	    this.onLink(progs, idx++, null, null);
	    return;
	}
	this.mPrograms.push(p);
	this.mProgressNode.value = idx+1;
	
	Log.d("Sending request for next videos of " + p.mId);
	NuLoader.request(API_URL+'/videos/'+p.mId+'?platform='+PLATFORM,true,
			 this.onLink.bind(this,progs,idx+1,p),
			 this.onLink.bind(this,progs,idx+1,p, null));
    }
    //_________________________________________________________________
    /** 
     * Write programs to the list. 
     */
    Serie.prototype.writePrograms = function() {
	var el = document.createElement("div")
	for (var i = 0; i < this.mPrograms.length; i++) 
	    el.appendChild(this.mPrograms[i].createElement());
	if (el.children.length <= 0) 
	    el.innerHTML = "Ingen programmer";
	this.mProgressNode.style.display = "none";
	// this.mLoading = false;
	this.mListNode.appendChild(el);
	Util.addClass(this.mListNode, "shown");
    }

    // ================================================================
    /** 
     * A program in a series 
     * 
     * @name Program 
     * @class 
     * @constructor 
     *
     * @param json JSON retreived from dr.dk
     * 
     * @return Newly allocated object 
     */
    var Program = function(json) {
	/**
         * The id
         * @fieldOf Program.prototype 
         */
	this.mId = json["id"];
	/**
         * The title
         * @fieldOf Program.prototype 
         */
	this.mTitle = json["title"];
	/**
         * The date
         * @fieldOf Program.prototype 
         */
	this.mDate = json["formattedBroadcastTime"];
	/**
         * Duration
         * @fieldOf Program.prototype 
         */
	this.mDuration = "";
	/**
         * Low resolution
         * @fieldOf Program.prototype 
         */
	this.mLow = null;
	/**
         * Medium
         * @fieldOf Program.prototype 
         */
	this.mMedium = null;
	/**
         * High
         * @fieldOf Program.prototype 
         */
	this.mHigh = null;
    };
    //_________________________________________________________________
    /** 
     * Check if we have all the information we need
     * 
     * @param {Boolean} testLinks If true, also test if we have any link
     * 
     * @return true on success
     */
    Program.prototype.isValid = function(testLinks) {
	if (this.mId == null || this.mId == "") return false;
	if (testLinks && !this.hasLinks()) return false;
	return true;
    };
    //_________________________________________________________________
    /** 
     * Check if we have any link 
     * 
     * @return true if at least one link is defiend
     */
    Program.prototype.hasLink = function() {
	return !(this.mLow    == null && 
		 this.mMedium == null && 
		 this.mHigh   == null);
    };
    //_________________________________________________________________
    /** 
     * Set the link to a stream
     * 
     * @param {String} which Resolution/bit-rate (low, medium, high)
     * @param {String}   url URL
     */
    Program.prototype.setLink = function(which,url) { 
	var u = null;
	if (url != null && url.length > 0)
	    u = ("http://vodfiles.dr.dk/" + 
		 url.substring(url.indexOf("CMS/Resources/"),
			       url.indexOf("?")));
	if (which == "low")    this.mLow    = u;
	if (which == "medium") this.mMediun = u;
	if (which == "high")   this.mHigh   = u;
    }
    //_________________________________________________________________
    /** 
     * Create a DOM element for this program 
     * 
     * @return Element 
     */
    Program.prototype.createElement = function(){
	var el = document.createElement("div");
	el.className = "serieProgram play";
	el.addEventListener("click", this.handle.bind(this));
	el.innerHTML = (" &nbsp;" + this.mTitle + "<br>" + 
			"<span class='serieDate'>" + this.mDate + "&#8193;" +
			this.mDuration + "</span>");
	return el;
    }
    //_________________________________________________________________
    /** 
     * Handle a click on this element.  This will actually play the
     * program on the receiver loaded in the ChromeCast. 
     */
    Program.prototype.handle = function() { 
	NuLoader.play(this.mLow, 
		      this.mMedium, 
		      this.mHigh, 
		      this.mTitle, 
		      this.mImg,
		      false);
    }
    //================================================================
    /** 
     * A category
     * 
     * @name Category 
     * @class 
     * @constructor 
     *
     * @param master  Reference to our NuLoader object
     * @param title   Title of this category 
     * @param topNode The top node of the navigation 
     * 
     * @return Newly allocated object of this class 
     */
    var Category = function(master,title,topNode) { 
	/**
         * Master
         * @fieldOf Category.prototype 
         */
	this.mMaster = master;
	/**
         * The Title
         * @fieldOf Category.prototype 
         */
	this.mTitle = title;
	/**
         * Sub-categories
         * @fieldOf Category.prototype 
         */
	this.mList = {};
	/**
         * Our master node
         * @fieldOf Category.prototype 
         */
	this.mNode = null;

	this.initialize(topNode)
    }
    //_________________________________________________________________
    /** 
     * Initialize 
     *
     * @param {HTMLElement} top Top level element of navigation 
     */
    Category.prototype.initialize = function(top) { 
	this.mNode = document.createElement("div");
	this.mNode.className="category";
	
	var tit = document.createElement("div");
	tit.className = "categoryTitle";
	tit.innerHTML = this.mTitle;
	tit.addEventListener("click", this.handle.bind(this));
	this.mNode.appendChild(tit);
	
	var lst = document.createElement("div");
	lst.className = "categoryList";
	this.mNode.appendChild(lst);

	top.appendChild(this.mNode);
    }
    //_________________________________________________________________
    /** 
     * Handle click on navigation element. This toggles the display of
     * the sub-category navigation elements.
     */
    Category.prototype.handle = function() { 
	if (Util.hasClass(this.mNode.children[0], "selected")) 
	    this.close();
	else 
	    this.open();
    }
    //_________________________________________________________________
    /** 
     * Add a sub-category
     * 
     * @param {SubCategory} sub Sub-category
     */
    Category.prototype.addSub = function(sub) { 
	this.mList[sub.mTitle] = sub; 
    }
    //_________________________________________________________________
    /** 
     * Add a serie to a sub-category of this category.  Note, we only
     * add the identifier.  The real Serie object is owned by the
     * master.
     * 
     * @param {String}    sub Sub-category name 
     * @param {String}   slug Serie slug.  
     */
    Category.prototype.addSerie = function(sub,slug) {
	if (this.mList[sub] == null) { 
	    Log.e("Sub-category " + sub + " of " + this.mTitle + " not found");
	    return;
	}
	Log.d("Adding serie to " + sub + " of " + this.mTitle);
	this.mList[sub].mList.push(slug);
    }
    //_________________________________________________________________
    /** 
     * Close all sub-categories of this category 
     */
    Category.prototype.closeAll = function() { 
	for (var i in this.mList) {
	    var sub = this.mList[i];
	    sub.close();
	}
    }
    //_________________________________________________________________
    /** 
     * Show this category - make sure to close other categories 
     */
    Category.prototype.open = function() { 
	this.mMaster.closeAll();
	Util.addClass(this.mNode.children[0], "selected");
	Util.addClass(this.mNode.children[1], "shown");

	var keys = Object.keys(this.mList);
	Log.d("This category has " + keys.length + " sub-categories");
	if (keys.length != 1) return;
	
	var id  = keys[0];
	Log.d("Will open sole sub-category " + id);
	this.mList[id].open();
    }
    //_________________________________________________________________
    /** 
     * Hide this category.  This will also close all sub-categories 
     */
    Category.prototype.close = function() { 
	Util.removeClass(this.mNode.children[0], "selected");
	Util.removeClass(this.mNode.children[1], "shown");
	this.closeAll();
    }
	
    //================================================================
    /** 
     * A sub-category 
     * 
     * @name SubCategory 
     * @class 
     * @constructor 
     *
     * @param {Category}      parent Parent category 
     * @param {String}       title Title of this sub-category 
     * @param {HTMLElement}  dispParent Parent element of the display element. 
     * @param {String}         url Url to load serie information from 
     * @param {Boolean} pseudo If this evaluates to true, then this is
     *                    a pseudo sub-category (i.e., for categories
     *                    that have no real sub-categories e.g., the
     *                    "Nyeste" category).
     * 
     * @return Newly allocated object of this class 
     */
    var SubCategory = function(parent,title,dispParent,url,pseudo) { 
	/**
         * The parent title
         * @fieldOf SubCategory.prototype 
         */
	this.mParent = null;
	/**
         * The sub-category title
         * @fieldOf SubCategory.prototype 
         */
	this.mTitle  = title;
	/**
         * Whether this is a pseudo element
         * @fieldOf SubCategory.prototype 
         */
	this.mPseudo = pseudo;
	/**
         * Request URL
         * @fieldOf SubCategory.prototype 
         */
	this.mUrl = url;
	/**
         * Whether we have loaded the slugs
         * @fieldOf SubCategory.prototype 
         */
	this.mLoaded = false;
	/**
         * The list of slugs
         * @fieldOf SubCategory.prototype 
         */
	this.mList = [];
	/**
         * Our element to display series in
         * @fieldOf SubCategory.prototype 
         */
	this.mDisplay = null;
	/** 
	 * Our navigation element 
	 * @fieldOf SubCategory.prototype 
	 */
	this.mButton = null;
	/** 
	 * Progress node 
	 * @fieldOf SubCategory.prototype 
	 */
	this.mProgressNode = null;
	/**
         * List node
         * @fieldOf SubCategory.prototype 
         */
	this.mListNode = null;
	
	this.initialize(parent, dispParent);
    }
    //_________________________________________________________________
    /** 
     * Intialization
     * 
     * @param navParent  Parent category
     * @param dispParent Display parent 
     */
    SubCategory.prototype.initialize = function(parent,dispParent)
    {
	this.createButton(parent);

	var parName = parent.mNode.children[0].innerHTML;
	this.createDisplay(parName, dispParent);
    }
    //_________________________________________________________________
    /** 
     * Createw our navigation button. 
     * 
     * @param {HTMLElement} parent Parent of the navigation button. 
     */
    SubCategory.prototype.createButton = function(parent) { 
	parent.addSub(this);
	this.mParent = parent;	
	this.mButton = document.createElement("div");
	this.mButton.className = "subCategory";
	this.mButton.innerHTML = this.mTitle;
	this.mButton.addEventListener("click",this.handle.bind(this));
	parent.mNode.children[1].appendChild(this.mButton);
    }
    //_________________________________________________________________
    /** 
     * Create our display element.
     * 
     * @param {String} parName Parent category name 
     * @param {HTMLElement}  parent Display elements parent element. 
     */
    SubCategory.prototype.createDisplay = function(parName,parent) { 
	var tit = "<b>"+parName+"</b>"
	if (this.mTitle != null && this.mTitle != "") 
	    tit += " &nbsp;" + this.mTitle;
	this.mDisplay = document.createElement("div");
	this.mDisplay.className = 'subCategoryView';
	
	var ti = document.createElement("div");
	ti.className = "subCategoryTitle";
	ti.innerHTML = tit;
	this.mDisplay.appendChild(ti);

	this.mProgressNode = document.createElement("div");
	this.mProgressNode.className = "subCategoryProgress";
	this.mProgressNode.innerHTML = "Indl&aelig;ser serier ...";
	this.mDisplay.appendChild(this.mProgressNode);

	this.mListNode = document.createElement("div");
	this.mListNode.className = "subCategoryList"
	this.mDisplay.appendChild(this.mListNode);

	parent.appendChild(this.mDisplay);
    }
    //_________________________________________________________________
    /** 
     * Open this sub-category.  This selects the navigation entry
     * and shows the serie display
     */
    SubCategory.prototype.open = function() { 
	this.mParent.closeAll();
	Util.addClass(this.mButton, "selected");
	Util.addClass(this.mDisplay, "shown");
	
	if (this.mLoaded) {
	    this.moveHere();
	    return;
	}
	this.load();
    }
    //_________________________________________________________________
    /** 
     * Get the information on this sub-category from the dr.dk. 
     * 
     * The load is done asynchronously
     */
    SubCategory.prototype.load = function() {
	Log.d("Will query " + this.mUrl);
	var url     = this.mUrl;
	NuLoader.request(url, true, 
			 this.onLoad.bind(this), 
			 this.onError.bind(this));
    }
    //_________________________________________________________________
    /** 
     * Call-back for load request.  This gets the JSON information
     * from the server and parses it.  It uses the service
     * NuLoader.parseSeries so that the master can keep track of
     * loaded series. It then uses the function SubCategory#moveHere
     * to move the series to the display element.  Note, the series
     * are never moved back to the cache.  Instead we rely on
     * Document#getElementById to find the e series for us - no matter
     * where in the DOM tree it is sitting.
     * 
     * @param json The data returned by the load request 
     */
    SubCategory.prototype.onLoad = function(json) {
	Log.d("Got back JSON object: " + JSON.stringify(json," "));
	var l    = (json ? json["ProgramSeries"] : null);
	if (l == null) {
	    this.onError();
	    return;
	}
	this.mParent.mMaster.parseSeries(l, this.mPseudo ? this : null);
	Log.d("Loaded the series for " + this.mTitle);
	this.mLoaded = true;
	this.moveHere();
    }
    //_________________________________________________________________
    /** 
     * Call-back in case of errors 
     */
    SubCategory.prototype.onError = function() { 
	this.mDisplay.children[1].innerHTML = "Ingen programmer fundet";
    }
    //_________________________________________________________________
    /** 
     * Move the series to our display element.  The serie elements are
     * found using Document#getElementById, so we never bother to move
     * the serie elements back to the cache.
     */
    SubCategory.prototype.moveHere = function() { 
	if (this.mList == null || this.mList.length == 0) { 
	    Log.e("List target for elements not found");
	    var txt = "Ingen programmer fundet";
	    if (this.mProgressNode != null)
		this.mProgressNode.innerHTML = txt;
	    else 
		this.mDisplay.children[1].innerHTML = txt;
	    return;
	}
	if (!this.mPseudo)
	    // Sort by title
	    this.mList.sort(function(a,b) { return a.localeCompare(b); });
	// Move here 
	Log.d("Will move a total of " + this.mList.length + " series here");
	for (var i = 0; i < this.mList.length; i++) {
	    var slug = this.mList[i];
	    var node = document.getElementById(slug);
	    if (node == null) {
		Log.e("Node: " + slug + " not found");
		continue;
	    }
	    var src = node.parentNode;
	    // The below would break (re-)sorting 
	    // if (src == tgt) {
	    //   this.log.warn("Source (" + src + ") and target (" + tgt
	    //                  + ") are the same");
	    //	    return;
	    // }
	    Log.d("Moving node " + slug + " here");
	    src.removeChild(node);
	    this.mListNode.appendChild(node);
	    var pl = this.mListNode.getElementsByClassName("seriePrograms");
	    for (var j = 0; j < pl.length; j++)
		Util.removeClass(pl[j], "shown");
	}
	if (this.mProgressNode != null)
	    Util.addClass(this.mProgressNode, "hidden");
    }
    //_________________________________________________________________
    /** 
     * Close this sub-category.  This de-selects the navigation entry
     * and closes the serie display
     */
    SubCategory.prototype.close = function() { 
	Util.removeClass(this.mButton, "selected");
	Util.removeClass(this.mDisplay, "shown");
    }
    //_________________________________________________________________
    /** 
     * Handle the button click in the navigation menu.  This toggles
     * the sub-category on/off.
     */
    SubCategory.prototype.handle = function() { 
	if (Util.hasClass(this.mButton, "selected")) {
	    this.close();
	}
	else { 
	    this.open();
	}
    }
    // ================================================================
    /** 
     * Sub-class of SubCategory for live streams 
     * 
     * @name LiveChannel 
     * @class 
     * @constructor 
     *
     * @param {Category}      parent Parent category 
     * @param {String}       title Title of this live channel
     * @param {HTMLElement}  dispParent Display element parent
     * @param {String}         url Url to fetch information from 
     * @param {String}      stream Stream to play 
     * 
     * @return Newly allocated object of this class 
     */
    var LiveChannel = function(parent,title,dispParent,url,stream) { 
	/** 
	 * Pointer to our `now' element 
	 * @fieldOf LiveChannel.prototype
	 */
	this.mNow = null;
	/** 
	 * Pointer to our `next' element 
	 * @fieldOf LiveChannel.prototype
	 */
	this.mNext = null;
	/** 
	 * Our live stream 	 
	 * @fieldOf LiveChannel.prototype
	 */
	this.mStream = stream;
	/** 
	 * Our timer
	 * @fieldOf LiveChannel.prototype */
	this.mTimer = null;

	/* Call super-class constructor in this scope */
	SubCategory.call(this,parent,title,dispParent,url);
    }
    //_________________________________________________________________
    /** 
     * Declare the inheritance.  We copy the protype here 
     */
    LiveChannel.prototype = Object.create(SubCategory.prototype);
    //_________________________________________________________________
    /** 
     * Create our display element  
     *
     * @overload SubChannel#createDisplay
     * 
     * @param {String}  parName Parent category name 
     * @param {HTMLElement}   parent Parent of the display element 
     */    
    LiveChannel.prototype.createDisplay = function(parName,parent) { 
	this.mDisplay = document.createElement("div");
	this.mDisplay.className = "liveView";
	
	this.mNow = document.createElement("div");
	this.mNow.className = "liveContent";
	this.mDisplay.appendChild(this.mNow);
	
	var ti = document.createElement("div");
	ti.className = "liveTitle";
	ti.innerHTML = "<b>" + parName + "</b> &nbsp;" + this.mTitle;
	this.mNow.appendChild(ti);

	var st = document.createElement("div");
	st.className = "liveStart";
	this.mNow.appendChild(st);

	var dr = document.createElement("div");
	dr.className = "liveDescription";
	this.mNow.appendChild(dr);

	var pl = document.createElement("div");
	pl.className = "livePlay play";
	pl.innerHTML = " &nbsp;Se Program";
	pl.addEventListener("click", this.play.bind(this));
	this.mNow.appendChild(pl);

	this.mNext = document.createElement("div");
	this.mNext.className = "liveNext";
	this.mDisplay.appendChild(this.mNext);
	
	var tg = document.createElement("span");
	tg.className = "liveNextTag";
	tg.innerHTML = "N&aelig;ste &nbsp;";
	this.mNext.appendChild(tg);

	var ns = document.createElement("span");
	ns.className = "liveNextStart";
	this.mNext.appendChild(ns);

	var nt = document.createElement("span");
	nt.className = "liveNextTitle";
	this.mNext.appendChild(nt);

	parent.appendChild(this.mDisplay);
    }
    //_________________________________________________________________
    /** 
     * Play this stream in the receiver loaded on the ChromeCast 
     * 
     */
    LiveChannel.prototype.play = function() { 
	// Create object 
	NuLoader.play(this.mStream,
		      this.mStream,
		      this.mStream,
		      this.mTitle + ": " + this.mNow.children[0].innerHTML,
		      "", 
		      true);

    }
    //_________________________________________________________________
    /** 
     * Show the display of this live channel. 
     * 
     * @overload SubChannel#open
     */
    LiveChannel.prototype.open = function() {
	Log.d("Will query " + this.mUrl + " for show information");
	this.mParent.closeAll();
	this.update("", "", "Indl&aelig;ser informationer ...", "", "");
	this.load();
	// SubCategory.prototype.open.call(this);
    }
    //_________________________________________________________________
    /** 
     * Close the display of this live channel. 
     * 
     * @overload SubChannel#close
     */
    LiveChannel.prototype.close = function() {
	Log.d("Closing " + this.mTitle + " clearing timer " + this.mTimer);
	if (this.mTimer) { 
	    clearTimeout(this.mTimer);
	    this.mTimer = null;
	}
	// Call super class close 
	SubCategory.prototype.close.call(this);
    }
    //_________________________________________________________________
    /** 
     * Load information from server. 
     * 
     * @overload SubChannel#load
     */
    LiveChannel.prototype.load = function() {
	Log.d("Loading from " + this.mUrl + " clearing possible timer " 
	      + this.mTimer);
	if (this.mTimer) { 
	    clearTimeout(this.mTimer);
	    this.mTimer = null;
	}
	// Call super class load 
	SubCategory.prototype.load.call(this);
    }
    //_________________________________________________________________
    /** 
     * Call-back for load request 
     * 
     * @overload SubChannel#onLoad
     *
     * @param json The returned data
     */
    LiveChannel.prototype.onLoad = function(json) {
	var delta = 60;
	if (json == null) { 
	    Log.w("Got no data on live channel request for " + this.mTitle);
	    this.reload(delta);
	    return;
	}
	var now  = json["Now"];
	var next = json["Next"];
	    
	if (now != null && next != null) {
	    this.update(now["Title"],
			now["FormattedBroadcastTime"],
			now["Description"], 
			next["Title"],
			next["FormattedBroadcastTime"]);

	    var cur = json["CurrentServerTimestamp"];
	    var end = now["EndTimestamp"];
	    Log.d("Time to next: " + end + " - " + cur + 
		  " = " + ((end-cur)/1000) + " seconds");
	    delta = Math.max(60,Math.min((end-cur)/1000,60*2));
	    this.reload(delta);
	}	
	else 
	    this.update("","","Ingen program informationer","","")

	Util.addClass(this.mButton, "selected");
	Util.addClass(this.mDisplay, "shown");
    }
    //_________________________________________________________________
    /** 
     * Update the display
     * 
     * @param request Our XMLHttpRequest 
     */
    LiveChannel.prototype.update = function(nowTitle,nowStart,nowDesc,
					    nextTitle,nextStart) {
	this.mNow.children[0].innerHTML  = nowTitle;
	this.mNow.children[1].innerHTML  = nowStart;
	this.mNow.children[2].innerHTML  = nowDesc;
	this.mNext.children[1].innerHTML = ("(" + nextStart + ") &nbsp;");
	this.mNext.children[2].innerHTML = nextTitle;
    }
    //_________________________________________________________________
    /** 
     * Reload the live stream information.  This is done in a timer to
     * make it asynchronous
     * 
     * @param delta Delay - in seconds - before loading.  
     */
    LiveChannel.prototype.reload = function(delta) {
	Log.d("Scheduling reload of this page in " + delta + " seconds");
	this.mTimer = setTimeout(this.load.bind(this), delta*1000);
    }
    //_________________________________________________________________
    /** 
     * A No-op
     * 
     * @overload SubChannel#moveHere
     */
    LiveChannel.prototype.moveHere = function() {}
    // ================================================================
    /** 
     * Sub-class of SubCategory for searches
     * 
     * @name Search 
     * @class 
     * @constructor 
     *
     * @param {Category}      parent Parent category 
     * @param {String}       title Title of this live channel
     * @param {Array}      genres List of genres
     * 
     * @return Newly allocated object of this class 
     */
    var Search = function(parent,dispParent,genres) { 
	/** 
	 * Title search field 
	 * @fieldOf {HTMLElement}  Search.prototype
	 */
	this.mText    = null;
	/** 
	 * Title search type 
	 * @fieldOf {HTMLElement}  Search.prototype
	 */
	this.mType    = null;
	/** 
	 * Channel search select
	 * @fieldOf {HTMLElement}  Search.prototype
	 */
	this.mChannel = null;
	/** 
	 * Genre search select
	 * @fieldOf {HTMLElement}  Search.prototype
	 */
	this.mGenre   = null;
	/** 
	 * List of genres
	 * @fieldOf {Array} Search.prototype
	 */
	this.mGList   = genres;
	/* Call super-class constructor in this scope */
	SubCategory.call(this,parent,"",dispParent,"",true);
    }
    //_________________________________________________________________
    /** 
     * Declare the inheritance.  We copy the protype here 
     */
    Search.prototype = Object.create(SubCategory.prototype);
    //_________________________________________________________________
    /** 
     * Create our display element  
     *
     * @overload SubChannel#createDisplay
     * 
     * @param {String}  parName Parent category name 
     * @param {HTMLElement}   parent Parent of the display element 
     */    
    Search.prototype.createDisplay = function(parName,parent) { 
	SubCategory.prototype.createDisplay.call(this,parName,parent);
	
	var form = document.createElement("div");
	form.className = "searchForm";
	
	var lab = document.createElement("label");
	lab.className = "searchLabel";
	lab.innerHTML = "Titel &nbsp;";
	form.appendChild(lab);
	
	this.mType = document.createElement("select");
	this.mType.className = "searchSelect";
	var opt = document.createElement("option");
	opt.text = "starter med";
	this.mType.add(opt, 0);
	opt = document.createElement("option");
	opt.text = "indeholder";
	this.mType.add(opt, 1);
	lab.appendChild(this.mType);
	
	this.mText = document.createElement("input");
	this.mText.className = "searchText";
	this.mText.setAttribute("type", "text");
	this.mText.onsubmit  = this.search.bind(this);
	this.mText.onkeypress = this.handleEnter.bind(this);

	lab.appendChild(this.mText);
	
	lab = document.createElement("label");
	lab.className = "searchLabel";
	lab.innerHTML = "p&aring kanal ";
	form.appendChild(lab);
	
	this.mChannel = document.createElement("select");
	this.mChannel.className = "searchSelect";

	opt = document.createElement("option");
	opt.text = "Alle";
	this.mChannel.add(opt, 0);
	var idx = 1;
	for (var ch in ChMap) { 
	    var opt = document.createElement("option");
	    opt.text  = ChMap[ch];
	    opt.value = ch;
	    this.mChannel.add(opt, idx);
	}
	this.mChannel.selectedIndex = 0;
	lab.appendChild(this.mChannel);

	lab = document.createElement("label");
	lab.className = "searchLabel";
	lab.innerHTML = "i genren";
	form.appendChild(lab);

	this.mGenre = document.createElement("select");
	this.mGenre.className = "searchSelect";

	opt = document.createElement("option");
	opt.text = "Alle";
	this.mGenre.add(opt, 0);
	for (idx = 0; idx < this.mGList.length; idx++) { 
	    var txt = this.mGList[idx];
	    if (txt == "Ingen") continue;
	    var opt = document.createElement("option");
	    opt.text  = txt;
	    opt.value = txt;
	    this.mGenre.add(opt, idx+1);
	}
	this.mGenre.selectedIndex = 0;
	lab.appendChild(this.mGenre);
	
	var but = document.createElement("button");
	but.className = "searchButton";
	but.innerHTML = "S&oslash;g ...";
	but.onclick   = this.search.bind(this);
	form.appendChild(but);
	
	this.mDisplay.insertBefore(form, this.mProgressNode);
    }
    //_________________________________________________________________
    /** 
     * Handle key-event
     * 
     * @param e Event
     */
    Search.prototype.handleEnter = function (e) {
	if (e == null && window.event) e = window.event;
	if (e.keyCode == 13) this.search();
    }
    //_________________________________________________________________
    /** 
     * Perform the search 
     */
    Search.prototype.search = function() { 
	Log.i("Will search for " + this.mText.value + 
	      " channel "  + this.mChannel.value + 
	      " genre " + this.mGenre.value);
	Util.addClass(this.mProgressNode, "shown");
	this.mProgressNode.innerHTML = "S&oslash;ger ...";
	this.moveBack();
	var ti = this.mText.value;
	var ch = this.mChannel.selectedIndex == 0 ? "" : this.mChannel.value;
	var ge = this.mGenre.selectedIndex   == 0 ? "" : this.mGenre.value;
	var da = (ti.length > 0) ? false : true;
	var url = this.mParent.mMaster.queryUrl(ti,ge,ch,da,100,0);
	if (ti.length > 0 && this.mType.selectedIndex == 1) 
	    url = url.replace("startswith", "contains");
	this.mUrl = url;
	this.load();
    }
    //_________________________________________________________________
    /** 
     * Show the display of this live channel. 
     * 
     * @overload SubChannel#open
     */
    Search.prototype.open = function() {
	this.mParent.closeAll();
	Util.addClass(this.mButton, "selected");
	Util.addClass(this.mDisplay, "shown");	
	this.mProgressNode.innerHTML = "";
    }
    //_________________________________________________________________
    /** 
     * Close the display of this live channel. 
     * 
     * @overload SubChannel#close
     */
    Search.prototype.close = function() {
	Log.d("Closing search")
	// Call super class close 
	SubCategory.prototype.close.call(this);
	// We should move all elements back the to cache. 
    }
    //_________________________________________________________________
    /** 
     * Call-back for load request 
     * 
     * @overload SubChannel#onLoad
     *
     * @param json The returned data
     */
    Search.prototype.onLoad = function(json) {
	SubCategory.prototype.onLoad.call(this,json);
    }
    //_________________________________________________________________
    /** 
     * Move the series to our display element.  The serie elements are
     * found using Document#getElementById, so we never bother to move
     * the serie elements back to the cache.
     */
    SubCategory.prototype.moveBack = function() { 
	if (this.mList == null || this.mList.length == 0) { 
	    Log.e("Nothing to move back");
	    return;
	}
	var cache = document.getElementById("cache");
	Log.d("Will move a total of " + this.mList.length + " series back");
	for (var i = 0; i < this.mList.length; i++) {
	    var slug = this.mList[i];
	    var node = document.getElementById(slug);
	    if (node == null) {
		Log.e("Node: " + slug + " not found");
		continue;
	    }
	    var src = node.parentNode;
	    // The below would break (re-)sorting 
	    // if (src == tgt) {
	    //   this.log.warn("Source (" + src + ") and target (" + tgt
	    //                  + ") are the same");
	    //	    return;
	    // }
	    Log.d("Moving node " + slug + " to cache");
	    // Close first, then move 
	    var pl = this.mListNode.getElementsByClassName("seriePrograms");
	    for (var j = 0; j < pl.length; j++)
		Util.removeClass(pl[j], "shown");
	    src.removeChild(node);
	    cache.appendChild(node);
	}
	// Clear arrayt
	this.mList = [];
    }

    // ================================================================
    /**
     * Master class
     *
     * @name NuLoader 
     * @class 
     * @constructor 
     *
     */
    var NuLoader = function() {
	/** 
	 * Raw list of genres 
	 * @fieldOf {Array} NuLoader.prototype;
	 */
	this.mGenreList = null;
	/**
	 * List of series information
	 * @fieldOf {Object} NuLoader.prototype
	 */
	this.mSeries = {};
	/**
	 * List of LIVE channels
	 * @fieldOf {Category} NuLoader.prototype
	 */
	this.mLive = null;
	/**
	 * List of genres
	 * @fieldOf {Category} NuLoader.prototype
	 */
	this.mGenres = null;
	/**
	 * List of channels
	 * @fieldOf {Category} NuLoader.prototype
	 */
	this.mChannels = null;
	/**
	 * List of letters
	 * @fieldOf {Category} NuLoader.prototype
	 */
	this.mLetters = null;
	/**
	 * List of newest
	 * @fieldOf NuLoader.prototype
	 */
	this.mNewest = null;
	/**
	 * Search category 
	 * @fieldOf {Category} NuLoader.prototype
	 */
	this.mSearch = null;
	/**
	 * The progress text
	 * @fieldOf NuLoader.prototype
	 */
	this.mSplashText = document.getElementById("splashText");
	/**
	 * The progress text
	 * @fieldOf NuLoader.prototype
	 */
	this.mSplashBar = document.getElementById("splashBar");
	/**
	 * Veto for loading new stuff 
	 * @fieldOf NuLoader.prototype
	 */ 
	this.mLoading = false;

	this.initialize();
    };
    //_________________________________________________________________
    /**
     * Update progress display
     *
     * @param main  Main step
     * @param sub   Sub-step - if any
     */
    NuLoader.prototype.updateProgress = function(main, sub) {
	var txt = "(" + main;
	if (sub != null && sub.length > 0) txt += " - " + sub;
	txt += ")";
	this.mSplashText.innerHTML = txt;
	Log.d("Progress: " + txt);
	if (this.mSplashBar.max > 1) this.mSplashBar.value++;
    };
    //_________________________________________________________________
    /**
     * When done loading
     */
    NuLoader.prototype.initDone = function() {
	this.updateProgress("F&aelig;rdig", null);
	document.getElementById("splash").style.display = "none";
	document.getElementById("content").style.display = "block";

	Log.d("Initialization done");
    };
    //_________________________________________________________________
    /**
     * Initialize object.
     *
     * We parse the location object for options. 
     */
    NuLoader.prototype.initialize = function() {
	var search  = location.search;
	if (search && search.charAt(0) == '?') 
	    search = search.substring(1,search.length);
	var opts    = search.split('&');
	var options = { // Set defaults 
	    debug:  false
	};
	for (var i = 0; i < opts.length; i++) {
	    var pair = opts[i].split('=');
	    var key  = pair[0];
	    var val  = pair[1];
	    if (!key || key.length <= 0) continue;
	    if (!val || val.length <= 0) val = true;
	    options[key] = val;
	    Log.d("Got option: " + key + "=" + val);
	}
	if (!options["debug"]) 
	    /** @ignore */
	    Log.d = function() {};
	WrapXHR.setDebug(options["debug"]);
	
	this.updateProgress("Henter kategorier", null);

	var top = document.getElementById("categoryBrowser");
	var dis = document.getElementById("groupBrowser");

	this.setupLive(top,dis);
	this.setupNewest(top,dis);
	this.setupGenres(top,dis);
	this.setupChannels(top,dis);
	this.setupLetters(top,dis);
	this.setupSearch(top,dis);

	this.initDone();
    };
    //_________________________________________________________________
    /** 
     * Set-up the Live category 
     * 
     * @param {HTMLElement} top Top element of the navigation 
     * @param {HTMLElement} dis Top element of the display
     */
    NuLoader.prototype.setupLive = function(top,dis) { 
	this.mLive = new Category(this, "LIVE", top);

	var url  = "http://www.dr.dk/tv/external/channels?mediaType=tv";
	var json = NuLoader.request(url, false, null, null);
	if (!json) {
	    Log.e("Failed to load list of channels");
	    return;
	}
	var urls     = {};
	var channels = json["Data"];
	for (var ich in channels) { 
	    var ch = channels[ich];
	    if (ch["ChannelType"] != 0 || ch["WebChannel"] == true) {
		// Now the right kind of channel
		continue;
	    }

	    // Loop over the defined servers 
	    var slug    = ch["Slug"];
	    var servers = ch["StreamingServers"];
	    for (var isrv in servers) { 
		var srv = servers[isrv];
		if (srv["LinkType"] != "HLS") 
		    // Unly pick-up HTML Live Streaming server 
		    continue;
		var h = srv["Server"] + "/" 
		    + srv["Qualities"][0]["Streams"][0]["Stream"] 
		    + "&rebase=on";

		urls[slug] = h;
	    }
	}
	for (var ch in ChMap) { 
	    new LiveChannel(this.mLive, ChMap[ch], dis, 
			    "http://dr.dk/tv/api/live/info/" + ch + "/json", 
			    urls[ch]);
	}
    }
    //_________________________________________________________________
    /** 
     * Set-up newest
     * 
     * @param {HTMLElement} top Top element of the navigation 
     * @param {HTMLElement} dis Top element of the displa y
     */
    NuLoader.prototype.setupNewest = function(top,dis) {
	this.mNewest = new Category(this, "Nyeste", top);
	var url = this.queryUrl("", "", "", true, MAX_NEWEST, 0);
	new SubCategory(this.mNewest, "", dis, url, true);
    }
    //_________________________________________________________________
    /** 
     * Do a syncronious request to get list of genres 
     * 
     * @return Array of genres
     */
    NuLoader.prototype.getGenres = function() { 
	if (this.mGenreList != null && this.mGenreList.length > 0) 
	    return this.mGenreList;
	var json = NuLoader.request(API_URL + "genres", false, null, null);
	var ret  = [];
	for (var i=0; i < json.length; i++) { 
	    ret[i] = json[i]["name"];
	}
	ret[ret.length] = "Ingen";
	this.mGenreList = ret;
	return this.mGenreList;
    }
    //_________________________________________________________________
    /** 
     * Set-up genres 
     * 
     * @param {HTMLElement} top Top element of the navigation 
     * @param {HTMLElement} dis Top element of the displa y
     */
    NuLoader.prototype.setupGenres = function(top,dis) {
	var genres = this.getGenres();
	this.mGenres = new Category(this, "Genre", top);
	for (var id in genres) { 
	    var nam = genres[id];
	    var url = this.queryUrl("", nam, "", true, MAX_GENRE, 0);
	    var sub = new SubCategory(this.mGenres,nam,dis,url,false);
	}
    }
    //_________________________________________________________________
    /**
     * Get an array of letters 
     *
     * @return Array of letters
     */
    NuLoader.prototype.letterArray = function() {
	var ch = "-ABCDEFGHIJKLMNOPQRSTUVWXYZ";
	var ar = ch.split("");
	ar[ar.length] = "&AElig;"; // "Æ";
	ar[ar.length] = "&Oslash;"; // "Ø";
	ar[ar.length] = "&Aring;"; // "Å";
	return ar;
    };
    /** 
     * Set-up letters 
     * 
     * @param {HTMLElement} top Top element of the navigation 
     * @param {HTMLElement} dis Top element of the displa y
     */
    NuLoader.prototype.setupLetters = function(top,dis) {
	var letters = this.letterArray();

	this.mLetters = new Category(this, "Alfabetisk", top);
	for (var id in letters) { 
	    var ltt = letters[id];	    
	    var qlt = ltt;
	    // we need to pass Æ, Ø, and Å as the non-standard UTF-8
	    // %-encoded strings.  Regular two-byte %-codes doesn't
	    // work.
	    if      (ltt == "&AElig;")  qlt = "%u00C6";
	    else if (ltt == "&Oslash;") qlt = "%u00D8";
	    else if (ltt == "&Aring;")  qlt = "%u00C5";
	    
	    var url = this.queryUrl(qlt, "", "", false, MAX_LETTER, 0);
	    var sub = new SubCategory(this.mLetters,ltt,dis,url,false);
	}
    }
    //_________________________________________________________________
    /** 
     * Set-up channels
     * 
     * @param {HTMLElement} top Top element of the navigation 
     * @param {HTMLElement} dis Top element of the displa y
     */
    NuLoader.prototype.setupChannels = function(top,dis) {
	this.mChannels = new Category(this, "Kanaler", top);
	for (var id in ChMap) { 
	    var nam = ChMap[id];
	    var url = this.queryUrl("","",nam.toLowerCase(),true,MAX_CHANNEL,0);
	    Log.d("channel " + nam + "/" + id + " url: " + url);
	    var sub = new SubCategory(this.mChannels,nam,dis,url,false);
	}
    }
    //_________________________________________________________________
    /** 
     * Set-up search
     * 
     * @param {HTMLElement} top Top element of the navigation 
     * @param {HTMLElement} dis Top element of the displa y
     */
    NuLoader.prototype.setupSearch = function(top,dis) {
	var genres = this.getGenres();
	this.mSearch = new Category(this, "S&oslash;g", top);
	new Search(this.mSearch, dis, genres);
    }
    //_________________________________________________________________
    /**
     * Format a URL option.  Replace spaces with a '+'
     *
     * @param key   Option key 
     * @param value Option value
     *
     * @return string of encoded option.  
     */
    NuLoader.prototype.urlOption = function(key, value) {
	var str = (value == null) ? "" : value.toString();
	var val = str.replace(new RegExp("[ \\t]+", "g"), "+");
	var ret = "&" + key + "=" + val;
	return ret;
    };
    //_________________________________________________________________
    /** 
     * Write a query URL 
     * 
     * @param {String}    title Title part 
     * @param {String}    genre Genre part
     * @param {String}  channel Channel part
     * @param {Boolean}   byDate Sort by date (or not)
     * @param {Integer}    limit Maximum number of results
     * @param {Integer}   offset Off-set in the results 
     * 
     * @return String 
     */
    NuLoader.prototype.queryUrl = function(title, genre, channel, byDate, limit,
					   offset) {
        // date=true                            -> OK
        // date=true  + channel                 -> OK
        // date=true  + genre                   -> OK
        // date=true  + title                   -> OK (title ignored)
        // date=true  + channel + title         -> OK
        // date=true  + channel + genre         -> OK
        // date=true  + genre + title           -> OK (title ignored)
        // date=true  + channel + genre + title -> OK (channel+title ignored)
        // date=false                           -> empty
        // date=false + channel                 -> empty
        // date=false + genre                   -> empty
        // date=false + title                   -> OK
        // date=false + channel + genre         -> empty
        // date=false + channel + title         -> OK
        // date=false + genre + title           -> OK (genre ignored)
        // date=false + channel + genre + title -> OK (genre ignored)
	var ret = "http://www.dr.dk/tv/api/programmap?searchType=startswith";
	ret += this.urlOption("title", title);
	ret += this.urlOption("genre", genre);
	ret += this.urlOption("channelSlug", channel);
	ret += this.urlOption("includePreviews", "false");
	ret += this.urlOption("orderByDate", (byDate ? "true" : "false"));
	ret += this.urlOption("limit", limit);
	ret += this.urlOption("offset", offset);

	return ret;
    };
    //_________________________________________________________________
    /** 
     * Call-back on series requests.  This is called by
     * SubCategory#load.  We do this here (rather than in the
     * SubCategory object) so that this object is the owner of the
     * information and so that we can push the slugs to the other
     * categories as needed (e.g., the Genre)
     * 
     * @param {JSON}   l Returned data from query
     * @param {SubCategory} tgt If non-null, it is a SubCategory object 
     *            we should add directly to (this is used by the Newest pseudo
     *            category).
     */
    NuLoader.prototype.parseSeries = function(l, tgt) {
	var els = [];
	Log.d("Possible target: " + tgt);

	// Loop over programs
	var ok = 0;
	for (var i = 0; i < l.length; i++) {
	    // Get the object
	    var o = l[i];
	    if (o == null) {
		Log.e("Didn't get serie at " + i);
		continue;
	    }

	    // Find the slug
	    var slug = o["ProgramSeriesSlug"];
	    if (slug == null || slug.length <= 0) {
		slug = o["programSerieSlug"];
		if (slug == null || slug.length <= 0) {
		    Log.w("Didn't get slug of serie: " + o.toString());
		    continue;
		}
	    }
	    
	    // See if we have the series in the cache already
	    var s = this.mSeries[slug];
	    if (!(s == null)) {
		Log.d("Already have " + slug);
		if (tgt != null) {
		    Log.d("Adding " + slug + " to " + tgt.mTitle);
		    tgt.mList.push(slug);
		}
		continue;
	    }

	    // Create new series object
	    s = new Serie(o);
	    if (!s.isValid()) {
		this.log.error("Serie " + slug + " invalid: " + o.toString());
		continue;
	    }
	    ok++;
	    // If we asked to fill program information, do it here
	    // if (alsoPrograms) fillPrograms(s, alsoLinks);

	    // Add it to our cache
	    this.mSeries[slug] = s;

	    // Create the elements 
	    var el = s.createElement();
	    els.push(el);

	    // Register with sub-categories 
	    this.mGenres.addSerie(s.mGenre, slug);
	    this.mLetters.addSerie(s.mLetter, slug);
	    this.mChannels.addSerie(s.mChannel, slug);
	    if (tgt != null) {
		Log.d("Adding " + slug + " to " + tgt.mTitle);
		tgt.mList.push(slug);
	    }
	    
	}
	Log.d("Got a total of " + ok + "series");

	// Now add elements to the cache 
	var cache = document.getElementById("cache");
	for (var el in els) {
	    Log.d("Appending child " + el + ": " +  els[el]);
	    cache.appendChild(els[el]);
	}
    };
    //_________________________________________________________________
    /** 
     * Close all categories.  This is called by Category#open to make
     * sure we only display one category. 
     */
    NuLoader.prototype.closeAll = function() {
	this.mLive.close();
	this.mNewest.close();
	this.mGenres.close();
	this.mChannels.close();
	this.mLetters.close();
	this.mSearch.close();
    };
    //_________________________________________________________________
    /** 
     * Toggle the display of the about box 
     */
    NuLoader.prototype.showAbout = function() { 
	var a = document.getElementById("aboutBox");
	if (a.style.display == "block") {
	    a.style.display = "none";
	}
	else {
	    a.style.display = "block";
	}
    }
    //_________________________________________________________________
    /** 
     * Play a stream 
     * 
     * @param {String}     low Low band-width URL
     * @param {String}  medium Medium band-width URL
     * @param {String}    high High band-width URL
     * @param {String}   title Title of stream
     * @param {String}   thumb Possible thumb-image URL
     * @param {Boolean}    live If true, assume live streaming 
     */
    NuLoader.play = function(low,medium,high,title,thumb,live) {
	var obj = new Object;
	obj["low"]    = low;
	obj["medium"] = medium;
	obj["high"]   = high;
	obj["title"]  = title;
	obj["thumb"]  = thumb;

	Log.i("Will queue " + (live ? "(live)" : "") + ": " + 
	      JSON.stringify(obj, " "));
	var ctrl = document.getElementById('iplayer').contentWindow.CastControl;
	ctrl.disableSeeks(live);
	ctrl.selectMedia(obj);
    }
    //_________________________________________________________________
    NuLoader.lock = false;
    //_________________________________________________________________
    /** 
     * Make an synchronous or a-synchronous GET XMLHttpRequest to @a url. 
     * 
     * @param {URL}      url URL to request
     * @param {Boolean}    async If true, do an a-synchronous request
     * @param {function}  success If @a async is true, the success call-back
     * @param {function}    errorIf @a async is false, the error call-back
     * 
     * @return If @a async is true, always null.  If @a async is fall,
     * the retreived JSON data or null in case of errors.
     */
    NuLoader.request = function(url,async,success,error,test)
    {
	if (NuLoader.lock) { 
	    Log.e("Busy, try later");
	    alert("Siden henter informationer. Pr&oslash;v senere");
	    return;
	}
	NuLoader.lock = true;
	Log.d("Doing a"+(async ? "n a" : " ")+ "synchronous GET request to " + 
	      url + " with call-backs " + success + " and " + error);

	var req = new XMLHttpRequest();
	req.open("GET", url, async);
	if (async)
	    /** @ignore */
	    req.onreadystatechange = function() { 
		NuLoader.onRequest(req, success, error, test);
	    };
	req.send();
	if (async) return null;

	if (req.responseText == null || req.responseText.length <= 0) {
	    Log.w("Synchronous GET request to " + url + 
		  " gave empty response: " + req.responseText + 
		  "(state=" + req.readyState + " status=" +
		  req.status + " text=" + req.statusText + ")");
	    return null;
	}
	Log.d("Got response " + req.responseText)
	NuLoader.lock = false;
	return Util.parseResponse(req.responseText);
    }
    /** 
     * Call-back for requests 
     * 
     * @param {XMLHttpRequest} request Request object 
     * @param {function} success User defined call-back for success 
     * @param {function}   error User defined call-back for errors
     */
    NuLoader.onRequest = function(request,success,error,test) { 
	Log.d("Request call-back w/" + request.readyState + 
	     " arguments " + JSON.stringify(arguments, " "));
	// Possible values of readyState 
	//  0 - not send yet
	//  1 - Opened (send called)
	//  2 - Headers received 
	//  3 - Loading the results 
	//  4 - Operation is complete 
	if (request.readyState != 4) 
	    // Wait until request completed 
	    return 
	var data = null;
	if (request.status != 200) { 
	    // We didn't get what we're looking for 
	    Log.w("Request failed: " + request.status + 
		  " '" + request.statusText + "'");
	    data = null;
	}
	else if (request.responseText == null || 
		 request.responseText.length <= 0) {
	    Log.w("Got an empty response");
	    data = null;
	}
	else if (test) { 
	    data = request.responseText;
	}
	else {
	    data = Util.parseResponse(request.responseText);
	}
	NuLoader.lock = false;
	if (data == null && error) error();
	if (success) success(data);
    }
    /* Make this class visible to the document. */
    window.NuLoader = NuLoader;
})();

/* 
 * EOF 
 */
